﻿using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using System.Windows.Markup;
using System.Xaml;
using SL5.Common;

namespace System.Windows.Data
{
	/// <summary>
	/// Describes a collection of <see cref="T:System.Windows.Data.BindingBase" /> objects attached to a single binding target property.
	/// </summary>
	[ContentProperty("Bindings")]
	public sealed class MultiBinding : MarkupExtension
	{
		#region Private Static Variables 
		private static readonly PropertyInfo _setterValueProperty = typeof(Setter).GetProperty("Value", BindingFlags.Public | BindingFlags.Instance);
		private static readonly Dictionary<MemberInfo, DependencyProperty> _dependencyProperties = new Dictionary<MemberInfo, DependencyProperty>();
		#endregion Private Static Variables 

		#region Private Static Methods 
		/// <summary>
		/// Gets the dependency property the member is pointing to.
		/// </summary>
		/// <param name="member">The member.</param>
		/// <returns>Dependency property.</returns>
		private static DependencyProperty GetDependencyProperty(MemberInfo member)
		{
			lock (MultiBinding._dependencyProperties)
			{
				DependencyProperty dependencyProperty;
				if (!MultiBinding._dependencyProperties.TryGetValue(member, out dependencyProperty))
				{
					string propertyName = member.MemberType == MemberTypes.Property ? member.Name : member.Name.Substring(3);
					FieldInfo field = member.DeclaringType.GetField(propertyName + "Property", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
					if (field == null || field.FieldType != typeof(DependencyProperty))
					{
						throw new MissingMemberException("Failed to find dependency property for " + member);
					}
					MultiBinding._dependencyProperties.Add(member, dependencyProperty = (DependencyProperty)field.GetValue(null));
				}
				return dependencyProperty;
			}
		}
		#endregion Private Static Methods 

		#region Private Variables 
		private readonly BindingCollection _bindings;
		private BindingMode _mode;
		private bool _isSealed;
		private CultureInfo _converterCulture;
		private IMultiValueConverter _converter;
		private object _converterParameter;
		private object _fallbackValue;
		private object _targetNullValue;
		private string _stringFormat;
		#endregion Private Variables 

		#region Public Properties 
		/// <summary>
		/// Gets the collection of <see cref="T:System.Windows.Data.BindingBase" /> objects within this <see cref="T:System.Windows.Data.MultiBinding" /> instance.
		/// </summary>
		public Collection<BindingBase> Bindings
		{
			get
			{
				return this._bindings;
			}
		}

		/// <summary>
		/// Gets or sets the converter to use to convert the source values to or from the target value.
		/// </summary>
		public IMultiValueConverter Converter
		{
			get
			{
				return this._converter;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._converter = value;
			}
		}

		/// <summary>
		/// Gets or sets the <see cref="T:System.Globalization.CultureInfo" /> object that applies to any converter assigned to bindings wrapped by the <see cref="T:System.Windows.Data.MultiBinding" /> or on the <see cref="T:System.Windows.Data.MultiBinding" /> itself.
		/// </summary>
		public CultureInfo ConverterCulture
		{
			get
			{
				return this._converterCulture ?? CultureInfo.CurrentCulture;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._converterCulture = value;
			}
		}

		/// <summary>
		/// Gets or sets an optional parameter to pass to a converter as additional information.
		/// </summary>
		public object ConverterParameter
		{
			get
			{
				return this._converterParameter;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._converterParameter = value;
			}
		}

		/// <summary>
		/// Gets or sets a value to use when the binding is unable to return a value.
		/// </summary>
		public object FallbackValue
		{
			get
			{
				return this._fallbackValue;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._fallbackValue = value;
			}
		}

		/// <summary>
		/// Gets or sets a value that indicates the direction of the data flow in the binding.
		/// </summary>
		public BindingMode Mode
		{
			get
			{
				return this._mode;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._mode = value;
			}
		}

		/// <summary>
		/// Gets or sets a string that specifies how to format the binding if it displays the bound value as a string.
		/// </summary>
		public string StringFormat
		{
			get
			{
				return this._stringFormat;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._stringFormat = value;
			}
		}

		/// <summary>
		/// Gets or sets the value that is used in the target when the value of the source is null.
		/// </summary>
		public object TargetNullValue
		{
			get
			{
				return this._targetNullValue;
			}
			set
			{
				this.VerifyIsNotSealed();
				this._targetNullValue = value;
			}
		}
		#endregion Public Properties 

		#region Private Methods 
		/// <summary>
		/// Verifies this instance is not sealed.
		/// </summary>
		private void VerifyIsNotSealed()
		{
			if (this._isSealed)
			{
				throw new InvalidOperationException("Can not modify MultiBinding once it is sealed.");
			}
		}
		#endregion Private Methods 

		#region Public Methods 
		/// <summary>
		/// When implemented in a derived class, returns an object that is provided as the value of the target property for this markup extension.
		/// </summary>
		/// <param name="serviceProvider">A service provider helper that can provide services for the markup extension.</param>
		/// <returns>The object value to set on the property where the extension is applied.</returns>
		public override object ProvideValue(IServiceProvider serviceProvider)
		{
			if (this.Converter == null && String.IsNullOrEmpty(this.StringFormat))
			{
				throw new InvalidOperationException("A MultiBinding requires a converter or a string format to operate.");
			}
			if (this.Converter == null && this.Mode == BindingMode.TwoWay)
			{
				throw new InvalidOperationException("A TwoWay MultiBinding can not operate without a converter.");
			}
			if (this.Bindings.Count == 0)
			{
				throw new InvalidOperationException("There are no source bindings.");
			}
			IProvideValueTarget targetProvider = serviceProvider.GetService<IProvideValueTarget>();
			MemberInfo member = targetProvider.TargetProperty as MemberInfo;
			if (member == null || (member.MemberType != MemberTypes.Property && member.MemberType != MemberTypes.Method))
			{
				throw new InvalidOperationException("Unexpected target property.");
			}
			bool isSetterValue = member == MultiBinding._setterValueProperty;
			DependencyObject dependencyObject = (isSetterValue ? serviceProvider.GetService<IRootObjectProvider>().RootObject : targetProvider.TargetObject) as DependencyObject;
			if (dependencyObject == null)
			{
				throw new NotSupportedException("MultiBinding only supports dependency objects");
			}
			this._isSealed = true;
			MultiBindingOperation operation = new MultiBindingOperation(this, dependencyObject);
			if (isSetterValue)
			{
				return operation.Binding;
			}
			BindingOperations.SetBinding(dependencyObject, MultiBinding.GetDependencyProperty(member), operation.Binding);
			return operation.Binding.ProvideValue(serviceProvider);
		}
		#endregion Public Methods 

		#region Public Constructors 
		/// <summary>
		/// Initializes a new instance of the <see cref="MultiBinding"/> class.
		/// </summary>
		public MultiBinding()
		{
			this._bindings = new BindingCollection(this);
		}
		#endregion Public Constructors 

		#region Nested Classes 
		/// <summary>
		/// Binding collection.
		/// </summary>
		private sealed class BindingCollection : Collection<BindingBase>
		{
			#region Private Variables 
			private readonly MultiBinding _multiBinding;
			#endregion Private Variables 

			#region Protected Methods 
			/// <summary>
			/// Removes all elements from the <see cref="T:System.Collections.ObjectModel.Collection`1"/>.
			/// </summary>
			protected override void ClearItems()
			{
				this._multiBinding.VerifyIsNotSealed();
				base.ClearItems();
			}

			/// <summary>
			/// Inserts an element into the <see cref="T:System.Collections.ObjectModel.Collection`1"/> at the specified index.
			/// </summary>
			/// <param name="index">The zero-based index at which <paramref name="item"/> should be inserted.</param>
			/// <param name="item">The object to insert. The value can be null for reference types.</param>
			/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index"/> is less than zero.
			/// -or-
			///   <paramref name="index"/> is greater than <see cref="P:System.Collections.ObjectModel.Collection`1.Count"/>.
			///   </exception>
			protected override void InsertItem(int index, BindingBase item)
			{
				this._multiBinding.VerifyIsNotSealed();
				base.InsertItem(index, item);
			}

			/// <summary>
			/// Removes the element at the specified index of the <see cref="T:System.Collections.ObjectModel.Collection`1"/>.
			/// </summary>
			/// <param name="index">The zero-based index of the element to remove.</param>
			/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index"/> is less than zero.
			/// -or-
			///   <paramref name="index"/> is equal to or greater than <see cref="P:System.Collections.ObjectModel.Collection`1.Count"/>.
			///   </exception>
			protected override void RemoveItem(int index)
			{
				this._multiBinding.VerifyIsNotSealed();
				base.RemoveItem(index);
			}

			/// <summary>
			/// Replaces the element at the specified index.
			/// </summary>
			/// <param name="index">The zero-based index of the element to replace.</param>
			/// <param name="item">The new value for the element at the specified index. The value can be null for reference types.</param>
			/// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="index"/> is less than zero.
			/// -or-
			///   <paramref name="index"/> is greater than <see cref="P:System.Collections.ObjectModel.Collection`1.Count"/>.
			///   </exception>
			protected override void SetItem(int index, BindingBase item)
			{
				this._multiBinding.VerifyIsNotSealed();
				base.SetItem(index, item);
			}
			#endregion Protected Methods 

			#region Public Constructors 
			/// <summary>
			/// Initializes a new instance of the <see cref="BindingCollection"/> class.
			/// </summary>
			/// <param name="multiBinding">The multi binding.</param>
			public BindingCollection(MultiBinding multiBinding)
			{
				this._multiBinding = multiBinding;
			}
			#endregion Public Constructors 
		}

		/// <summary>
		/// Multi binding operation.
		/// </summary>
		[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
		public sealed class MultiBindingOperation : IValueConverter, INotifyPropertyChanged
		{
			#region Private Constants 
			private const int GENERIC_PROPERTY_COUNT = 255;
			#endregion Private Constants 

			#region Private Static Variables 
			private static readonly DependencyProperty[] _genericProperties = MultiBindingOperation.CreateGenericProperties();
			private static readonly DependencyProperty _operationsProperty = DependencyProperty.RegisterAttached("Operations", typeof(IList<MultiBindingOperation>), typeof(MultiBindingOperation), new PropertyMetadata(null));
			#endregion Private Static Variables 

			#region Private Static Methods 
			/// <summary>
			/// Creates the generic attached properties.
			/// </summary>
			/// <returns>Dependency property array.</returns>
			private static DependencyProperty[] CreateGenericProperties()
			{
				DependencyProperty[] array = new DependencyProperty[MultiBindingOperation.GENERIC_PROPERTY_COUNT];
				for (int index = 0; index < array.Length; index++)
				{
					array[index] = DependencyProperty.RegisterAttached(
						"Property" + index, 
						typeof(object), 
						typeof(MultiBindingOperation), 
						new PropertyMetadata(MultiBindingOperation.GenericProperty_PropertyChanged)
					);
				}
				return array;
			}

			/// <summary>
			/// Callled when one of the generic attached properties has its value changed.
			/// </summary>
			/// <param name="d">The object the dependency property is attached to.</param>
			/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
			private static void GenericProperty_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
			{
				int propertyIndex = Array.IndexOf(MultiBindingOperation._genericProperties, e.Property);
				foreach (MultiBindingOperation operation in MultiBindingOperation.GetOperations(d))
				{
					if (operation._startIndex <= propertyIndex && operation._endIndex >= propertyIndex)
					{
						operation.Values[propertyIndex - operation._startIndex] = e.NewValue;
						operation.OnValuesChanged();
						break;
					}
				}
			}

			/// <summary>
			/// Gets the operations for a dependency object.
			/// </summary>
			/// <param name="d">The dependency object to fetch the operations for.</param>
			/// <returns>List of operations.</returns>
			private static IList<MultiBindingOperation> GetOperations(DependencyObject d)
			{
				IList<MultiBindingOperation> collection = (IList<MultiBindingOperation>)d.GetValue(MultiBindingOperation._operationsProperty);
				if (collection == null)
				{
					d.SetValue(MultiBindingOperation._operationsProperty, collection = new List<MultiBindingOperation>());
				}
				return collection;
			}
			#endregion Private Static Methods 

			#region Private Variables 
			private readonly Binding _binding;
			private readonly DependencyObject _targetObject;
			private readonly int _endIndex;
			private readonly int _startIndex;
			private readonly MultiBinding _multiBinding;
			private object[] _values;
			private readonly Type[] _targetTypes;
			#endregion Private Variables 

			#region Public Properties 
			/// <summary>
			/// Gets the binding.
			/// </summary>
			internal Binding Binding
			{
				get
				{
					return this._binding;
				}
			}

			/// <summary>
			/// Gets the values.
			/// </summary>
			public object[] Values
			{
				get
				{
					return this._values ?? (this._values = new object[this._multiBinding.Bindings.Count]);
				}
				set
				{
					if (this._values != value)
					{
						this._values = value;
						this.OnValuesChanged();
					}
				}
			}
			#endregion Public Properties 

			#region Private Methods 
			object IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture)
			{
				object[] values = value as object[];
				if (values != null)
				{
					if (this._multiBinding.Converter != null)
					{
						value = this._multiBinding.Converter.Convert(values, targetType, parameter, culture);
						if (!targetType.IsAssignableFrom(typeof(string)) || String.IsNullOrEmpty(this._multiBinding.StringFormat))
						{
							return value;
						}
						values = new[] { value };
					}
					if (!String.IsNullOrEmpty(this._multiBinding.StringFormat))
					{
						return String.Format(culture, this._multiBinding.StringFormat, values);
					}
				}
				return DependencyProperty.UnsetValue;
			}

			object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
			{
				if (this._multiBinding.Converter == null)
				{
					return DependencyProperty.UnsetValue;
				}
				object[] array = this._multiBinding.Converter.ConvertBack(value, this._targetTypes, parameter, culture);
				return array == null || array.Length != this._targetTypes.Length ? null : array;
			}

			/// <summary>
			/// Called when the values are changed.
			/// </summary>
			private void OnValuesChanged()
			{
				PropertyChangedEventHandler handler = this.PropertyChanged;
				if (handler != null)
				{
					handler(this, new PropertyChangedEventArgs("Values"));
				}
			}
			#endregion Private Methods 

			#region Public Events 
			/// <summary>
			/// Occurs when a property value changes.
			/// </summary>
			public event PropertyChangedEventHandler PropertyChanged;
			#endregion Public Events 

			#region Private Constructors 
			/// <summary>
			/// Releases unmanaged resources and performs other cleanup operations before the
			/// <see cref="MultiBindingOperation"/> is reclaimed by garbage collection.
			/// </summary>
			~MultiBindingOperation()
			{
				Deployment.Current.Dispatcher.BeginInvoke(() =>
				{
					for (int index = 0; index < this._values.Length; index++)
					{
						this._targetObject.ClearValue(MultiBindingOperation._genericProperties[this._startIndex + index]);
					}
				});
			}
			#endregion Private Constructors 

			#region Public Constructors 
			/// <summary>
			/// Initializes a new instance of the <see cref="MultiBindingOperation"/> class.
			/// </summary>
			/// <param name="multiBinding">The multi binding.</param>
			/// <param name="targetObject">The target object.</param>
			internal MultiBindingOperation(MultiBinding multiBinding, DependencyObject targetObject)
			{
				IList<MultiBindingOperation> operations = MultiBindingOperation.GetOperations(targetObject);
				this._startIndex = operations.Count != 0 ? operations[operations.Count - 1]._endIndex + 1 : 0;
				if ((this._startIndex + multiBinding.Bindings.Count - 1) >= MultiBindingOperation.GENERIC_PROPERTY_COUNT)
				{
					throw new ArgumentException("The maximum number of properties bound to a single object through a MultiBinding has been exceeded.");
				}
				operations.Add(this);
				this._values = new object[multiBinding.Bindings.Count];
				this._endIndex = this._startIndex + this._values.Length - 1;
				this._multiBinding = multiBinding;
				this._targetObject = targetObject;
				this._targetTypes = new Type[this._values.Length];
				for (int index = 0; index < this._values.Length; index++)
				{
					this._targetTypes[index] = typeof(object);
					BindingOperations.SetBinding(targetObject, MultiBindingOperation._genericProperties[this._startIndex + index], multiBinding.Bindings[index]);
				}
				this._binding = new Binding("Values")
				{
					Source = this,
					Converter = this,
					ConverterCulture = multiBinding.ConverterCulture,
					ConverterParameter = multiBinding.ConverterParameter,
					FallbackValue = multiBinding.FallbackValue,
					TargetNullValue = multiBinding.TargetNullValue,
					Mode = multiBinding.Mode
				};
			}
			#endregion Public Constructors 
		}
		#endregion Nested Classes 
	}
}